/*******************************************************************************
 * Copyright 2013 Mindaugas Idzelis
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package incontrol.rcon.protocol;

import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.toArray;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;

public class Packet {

	private final Origin origin;
	private final Type type;
	private final long sequence;
	private Word[] words;

	public Packet(Origin origin, Type type, long sequence, Iterable<Word> words) {
		this.origin = origin;
		this.type = type;
		this.sequence = sequence;
		Iterable<Word> wordsI = filter(words, Predicates.notNull());
		this.words = toArray(wordsI, Word.class);
	}

	public Packet(Origin origin, Type type, long sequence, Word cmd,
			Iterable<Word> args) {
		this(origin, type, sequence, concat(singleton(cmd), args));
	}

	public Packet(Origin origin, Type type, long sequence, String cmd,
			String... args) {
		this(origin, type, sequence, transform(
				concat(singleton(cmd), asList(args)),
				new Function<String, Word>() {
					public Word apply(String word) {
						if (word == null)
							return null;
						return new Word(word);
					}
				}));
	}

	public String[] getBodyAsString() {
		return Iterables.toArray(Iterables.transform(Arrays.asList(getBody()),
				new Function<Word, String>() {
					public String apply(Word input) {
						if (input == null)
							return null;
						return input.getValue();
					};
				}), String.class);

	}

	public Word[] getBody() {
		return words;
	}

	@Override
	public String toString() {
		String payload = Joiner.on(' ').join(
				Iterables.toArray(Iterables.transform(Arrays.asList(getBody()),
						new Function<Word, String>() {
							public String apply(Word input) {
								if (input == null)
									return null;
								return input.getValue();
							};
						}), String.class));

		return Objects.toStringHelper(Packet.class.getSimpleName())
				.add("seq", sequence).add("origin", origin.name())
				.add("type", type.name()).add("body", payload).toString();

	}

	public static Packet read(InputStream in) throws IOException {
		// ByteArrayOutputStream out = new ByteArrayOutputStream();
		// ByteStreams.copy(in, out);

		UInt32 requestSeq = UInt32.read(in);
		/* UInt32 packetSize = */UInt32.read(in);
		UInt32 numWords = UInt32.read(in);

		List<Word> words = newArrayList();
		for (int i = 0; i < numWords.getValue(); i++) {
			words.add(Word.read(in));
		}

		long requestSeqLong = requestSeq.getValue();
		boolean serverOrigin = ((requestSeqLong >>> 31) & 1) == 0;
		boolean request = ((requestSeqLong >>> 30) & 1) == 0;

		long seq = requestSeqLong;
		seq &= ~(1 << 31);
		seq &= ~(1 << 30);

		Packet r = new Packet(serverOrigin ? Origin.SERVER : Origin.CLIENT,
				request ? Type.REQUEST : Type.RESPONSE, seq, words);
		return r;
	}

	public void write(OutputStream out) throws IOException {
		// set 31 bit to 0 if origin is server, 1 if origin is client
		// set 30 bit to 0 for request, 1 for response
		long requestSeq = sequence;
		if (origin == Origin.SERVER) {
			requestSeq &= ~(1 << 31);
		} else {
			requestSeq |= 1 << 31;
		}
		if (type == Type.REQUEST) {
			requestSeq &= ~(1 << 30);
		} else {
			requestSeq |= 1 << 30;
		}

		int wordSize = 0;
		for (Word word : words) {
			wordSize += word.getSize();
		}

		// sequence
		new UInt32(requestSeq).write(out);
		// byte size of packet, plus 3 Uints32s
		new UInt32(wordSize + 12).write(out);
		// total words
		new UInt32(words.length).write(out);
		// words
		for (Word word : words) {
			word.write(out);
		}
	}

	public Origin getOrigin() {
		return origin;
	}

	public Type getType() {
		return type;
	}

	public long getSequence() {
		return sequence;
	}
}
